declare function require(moduleName: string): any;
declare const process: { cwd(): string };

const fs: any = require('fs');
const path: any = require('path');
import * as ts from 'typescript';

type CompilePluginResult = {
  code: string;
  map: null;
};

type CompilePlugin = {
  name: string;
  order?: 'first' | 'last';
  transform(this: any, code: string, id: string): Promise<CompilePluginResult | null> | CompilePluginResult | null;
};

type ExportEntry = {
  importName: string;
  reexportPath: string;
};

type ExportMap = Map<string, ExportEntry>;

const LOG_PREFIX = '[IndexExpandCompilePlugin]';
const INDEX_FILE_REGEX = /(?:^|\/)index(?:\.[^.]+)?$/i;
const EXPORT_MAP_CACHE = new Map<string, ExportMap | null>();
const FILE_CONTENT_CACHE = new Map<string, string>();
const FS_EXISTS_CACHE = new Map<string, boolean>();
const RESOLVE_CACHE = new Map<string, string | null>();
const SUPPORTED_INDEX_EXTENSIONS = ['.ets', '.ts', '.tsx', '.js', '.mjs'];
const MODULE_ROOT_CACHE = new Map<string, string | null>();
const PACKAGE_INFO_CACHE = new Map<string, any | null>();

function readFileCached(filePath: string): string | null {
  if (FILE_CONTENT_CACHE.has(filePath)) {
    return FILE_CONTENT_CACHE.get(filePath)!;
  }
  try {
    const content = fs.readFileSync(filePath, 'utf-8');
    FILE_CONTENT_CACHE.set(filePath, content);
    return content;
  } catch (error) {
    FILE_CONTENT_CACHE.set(filePath, '');
    return null;
  }
}

function pathExists(filePath: string): boolean {
  if (FS_EXISTS_CACHE.has(filePath)) {
    return FS_EXISTS_CACHE.get(filePath)!;
  }
  const exists = fs.existsSync(filePath);
  FS_EXISTS_CACHE.set(filePath, exists);
  return exists;
}

function normalizeToPosix(value: string): string {
  return value.split(path.sep).join('/');
}

function toWorkspaceRelative(filePath: string | null | undefined): string {
  if (!filePath) {
    return '(unknown)';
  }
  try {
    return normalizeToPosix(path.relative(process.cwd(), filePath));
  } catch (error) {
    return normalizeToPosix(filePath);
  }
}

function logInfo(message: string) {
  console.log(`${LOG_PREFIX} ${message}`);
}

function logWarn(message: string) {
  console.warn(`${LOG_PREFIX} ${message}`);
}

function shouldAttemptRewrite(moduleSpecifier: string): boolean {
  if (!moduleSpecifier) {
    return false;
  }
  const normalized = normalizeToPosix(moduleSpecifier);
  if (normalized.startsWith('.')) {
    return INDEX_FILE_REGEX.test(normalized);
  }
  if (INDEX_FILE_REGEX.test(normalized)) {
    return true;
  }
  const segments = normalized.split('/');
  const tail = segments[segments.length - 1] ?? normalized;
  if (/[A-Z]/.test(tail)) {
    return false;
  }
  return !normalized.includes('/');
}

function findModuleRoot(startPath: string): string | null {
  let dir = path.dirname(startPath);
  if (!dir || dir === '.') {
    dir = process.cwd();
  }
  const visited: string[] = [];
  while (true) {
    if (MODULE_ROOT_CACHE.has(dir)) {
      const cached = MODULE_ROOT_CACHE.get(dir)!;
      visited.forEach((entry) => MODULE_ROOT_CACHE.set(entry, cached));
      return cached;
    }
    visited.push(dir);
    const packageFiles = ['oh-package.json5', 'oh-package.json'];
    for (const fileName of packageFiles) {
      const candidate = path.join(dir, fileName);
      if (pathExists(candidate)) {
        visited.forEach((entry) => MODULE_ROOT_CACHE.set(entry, dir));
        return dir;
      }
    }
    const parent = path.dirname(dir);
    if (!parent || parent === dir) {
      visited.forEach((entry) => MODULE_ROOT_CACHE.set(entry, null));
      return null;
    }
    dir = parent;
  }
}

function parseJsonLike(content: string): any | null {
  if (!content) {
    return null;
  }
  try {
    return JSON.parse(content);
  } catch (_) {
    try {
      const factory = new Function(`return (${content});`);
      return factory();
    } catch (error) {
      return null;
    }
  }
}

function readPackageInfo(moduleRoot: string): any | null {
  if (PACKAGE_INFO_CACHE.has(moduleRoot)) {
    return PACKAGE_INFO_CACHE.get(moduleRoot)!;
  }
  const candidates = ['oh-package.json5', 'oh-package.json'];
  for (const fileName of candidates) {
    const packagePath = path.join(moduleRoot, fileName);
    if (!pathExists(packagePath)) {
      continue;
    }
    try {
      const content = fs.readFileSync(packagePath, 'utf-8');
      const parsed = parseJsonLike(content);
      if (parsed && typeof parsed === 'object') {
        PACKAGE_INFO_CACHE.set(moduleRoot, parsed);
        return parsed;
      }
    } catch (error) {
      PACKAGE_INFO_CACHE.set(moduleRoot, null);
      return null;
    }
  }
  PACKAGE_INFO_CACHE.set(moduleRoot, null);
  return null;
}

function resolveFromPackageDependency(moduleRoot: string, moduleSpecifier: string): string | null {
  const packageInfo = readPackageInfo(moduleRoot);
  if (!packageInfo || typeof packageInfo !== 'object') {
    return null;
  }
  const dependencies = packageInfo.dependencies;
  if (!dependencies || typeof dependencies !== 'object') {
    return null;
  }
  const target = dependencies[moduleSpecifier];
  if (!target || typeof target !== 'string') {
    return null;
  }
  if (!target.startsWith('file:')) {
    return null;
  }
  const relativePath = target.replace(/^file:/, '');
  const absolutePath = path.resolve(moduleRoot, relativePath);
  if (pathExists(absolutePath)) {
    return absolutePath;
  }
  return null;
}

function resolveFromModuleContext(moduleSpecifier: string, importer: string): string | null {
  const moduleRoot = findModuleRoot(importer);
  if (!moduleRoot) {
    return null;
  }
  const localOhModules = path.join(moduleRoot, 'oh_modules', moduleSpecifier);
  if (pathExists(localOhModules)) {
    return localOhModules;
  }
  const packagePath = resolveFromPackageDependency(moduleRoot, moduleSpecifier);
  if (packagePath) {
    return packagePath;
  }
  return null;
}

function toRelativeModulePath(baseSpecifier: string, reexportPath: string): string {
  if (!reexportPath.startsWith('.')) {
    return normalizeToPosix(reexportPath);
  }
  const cleanedBase = baseSpecifier.replace(/\/index(?:\.[^/]+)?$/i, '');
  const normalizedRelative = normalizeToPosix(path.posix.normalize(path.posix.join('.', reexportPath))).replace(/^\.\//, '');
  return cleanedBase ? `${cleanedBase}/${normalizedRelative}` : normalizedRelative;
}

function collectExportMap(indexPath: string): ExportMap | null {
  if (EXPORT_MAP_CACHE.has(indexPath)) {
    return EXPORT_MAP_CACHE.get(indexPath)!;
  }
  const content = readFileCached(indexPath);
  if (!content) {
    EXPORT_MAP_CACHE.set(indexPath, null);
    return null;
  }
  const sourceFile = ts.createSourceFile(indexPath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
  const exportMap: ExportMap = new Map();
  const importBindingMap = new Map<string, { importName: string; modulePath: string }>();

  sourceFile.forEachChild((node) => {
    if (!ts.isImportDeclaration(node) || !node.importClause || !node.moduleSpecifier) {
      return;
    }
    if (!ts.isStringLiteralLike(node.moduleSpecifier)) {
      return;
    }
    const modulePath = node.moduleSpecifier.text;
    const importClause = node.importClause;
    if (!importClause.namedBindings || !ts.isNamedImports(importClause.namedBindings)) {
      return;
    }
    importClause.namedBindings.elements.forEach((element) => {
      const localName = element.name.getText(sourceFile);
      const importName = element.propertyName ? element.propertyName.getText(sourceFile) : localName;
      importBindingMap.set(localName, { importName, modulePath });
    });
  });

  sourceFile.forEachChild((node) => {
    if (!ts.isExportDeclaration(node) || !node.exportClause) {
      return;
    }
    if (!ts.isNamedExports(node.exportClause)) {
      return;
    }
    if (node.moduleSpecifier) {
      if (!ts.isStringLiteralLike(node.moduleSpecifier)) {
        return;
      }
      const modulePath = node.moduleSpecifier.text;
      node.exportClause.elements.forEach((element) => {
        const exportedName = element.name.getText(sourceFile);
        const importName = element.propertyName ? element.propertyName.getText(sourceFile) : exportedName;
        exportMap.set(exportedName, {
          importName,
          reexportPath: modulePath,
        });
      });
      return;
    }
    node.exportClause.elements.forEach((element) => {
      const exportedName = element.name.getText(sourceFile);
      const localName = element.propertyName ? element.propertyName.getText(sourceFile) : exportedName;
      const binding = importBindingMap.get(localName);
      if (!binding) {
        return;
      }
      exportMap.set(exportedName, {
        importName: binding.importName,
        reexportPath: binding.modulePath,
      });
    });
  });

  EXPORT_MAP_CACHE.set(indexPath, exportMap);
  return exportMap;
}

function ensureIndexFile(filePath: string): string | null {
  if (!filePath) {
    return null;
  }
  let statsIsDirectory = false;
  if (pathExists(filePath)) {
    try {
      statsIsDirectory = fs.statSync(filePath).isDirectory();
    } catch (error) {
      statsIsDirectory = false;
    }
  }
  if (statsIsDirectory) {
    for (const candidate of ['index', 'Index']) {
      for (const ext of SUPPORTED_INDEX_EXTENSIONS) {
        const attempt = path.join(filePath, `${candidate}${ext}`);
        if (pathExists(attempt)) {
          return attempt;
        }
      }
    }
    return null;
  }
  if (INDEX_FILE_REGEX.test(filePath) && pathExists(filePath)) {
    return filePath;
  }
  const hasExtension = Boolean(path.extname(filePath));
  if (!hasExtension) {
    for (const ext of SUPPORTED_INDEX_EXTENSIONS) {
      const attempt = `${filePath}${ext}`;
      if (INDEX_FILE_REGEX.test(attempt) && pathExists(attempt)) {
        return attempt;
      }
    }
    const dir = path.dirname(filePath);
    for (const candidate of ['index', 'Index']) {
      for (const ext of SUPPORTED_INDEX_EXTENSIONS) {
        const candidatePath = path.join(filePath, `${candidate}${ext}`);
        if (pathExists(candidatePath)) {
          return candidatePath;
        }
        const dirCandidate = path.join(dir, `${candidate}${ext}`);
        if (pathExists(dirCandidate)) {
          return dirCandidate;
        }
      }
    }
  }
  return null;
}

async function resolveIndexFile(thisContext: any, moduleSpecifier: string, importer: string): Promise<string | null> {
  const cacheKey = `${importer}::${moduleSpecifier}`;
  if (RESOLVE_CACHE.has(cacheKey)) {
    return RESOLVE_CACHE.get(cacheKey)!;
  }
  let resolvedId: string | null = null;
  try {
    const resolved = await thisContext.resolve(moduleSpecifier, importer, { skipSelf: true });
    if (resolved?.id) {
      resolvedId = resolved.id;
    }
  } catch (error) {
    resolvedId = null;
  }

  if (!resolvedId) {
    if (moduleSpecifier.startsWith('.')) {
      resolvedId = path.resolve(path.dirname(importer), moduleSpecifier);
    } else {
      const moduleContextCandidate = resolveFromModuleContext(moduleSpecifier, importer);
      if (moduleContextCandidate) {
        resolvedId = moduleContextCandidate;
      } else {
        const ohModulesCandidate = path.resolve(process.cwd(), 'oh_modules', moduleSpecifier);
        if (pathExists(ohModulesCandidate)) {
          resolvedId = ohModulesCandidate;
        } else {
          resolvedId = path.resolve(process.cwd(), moduleSpecifier);
        }
      }
    }
  }

  if (!resolvedId) {
    logWarn(`Failed to resolve '${moduleSpecifier}' for importer ${toWorkspaceRelative(importer)}`);
    RESOLVE_CACHE.set(cacheKey, null);
    return null;
  }

  const indexPath = ensureIndexFile(resolvedId);
  if (!indexPath) {
    logWarn(`Unable to locate index file for '${moduleSpecifier}' (importer: ${toWorkspaceRelative(importer)}, resolved: ${resolvedId ?? 'unknown'})`);
  }
  RESOLVE_CACHE.set(cacheKey, indexPath);
  return indexPath;
}

type SpecGroup = {
  value: string[];
  type: string[];
};

function buildImportStatement(source: string, specifiers: string[], isTypeImport = false): string {
  const uniqueSpecs = Array.from(new Set(specifiers));
  const importKeyword = isTypeImport ? 'import type' : 'import';
  return `${importKeyword} { ${uniqueSpecs.join(', ')} } from '${source}';`;
}

function formatSpecifier(importName: string, localName: string): string {
  return importName === localName ? importName : `${importName} as ${localName}`;
}

async function rewriteImportDeclaration(
  thisContext: any,
  node: ts.ImportDeclaration,
  sourceFile: ts.SourceFile,
  filePath: string
): Promise<{ start: number; end: number; text: string } | null> {
  if (!ts.isStringLiteralLike(node.moduleSpecifier)) {
    return null;
  }
  const importClause = node.importClause;
  if (!importClause) {
    return null;
  }
  if (!importClause.namedBindings || !ts.isNamedImports(importClause.namedBindings)) {
    return null;
  }
  const namedImports = importClause.namedBindings;
  if (namedImports.elements.length === 0) {
    return null;
  }
  const moduleSpecifier = node.moduleSpecifier.text;
  if (!shouldAttemptRewrite(moduleSpecifier)) {
    return null;
  }
  const indexPath = await resolveIndexFile(thisContext, moduleSpecifier, filePath);
  if (!indexPath) {
    return null;
  }
  const exportMap = collectExportMap(indexPath);
  if (!exportMap || exportMap.size === 0) {
    return null;
  }

  const groupedImports = new Map<string, SpecGroup>();
  const remainingValueSpecifiers: string[] = [];
  const remainingTypeSpecifiers: string[] = [];
  let hasRewrite = false;
  const clauseIsTypeOnly = Boolean(importClause.isTypeOnly);

  for (const element of namedImports.elements) {
    const requestedName = element.propertyName ? element.propertyName.getText(sourceFile) : element.name.getText(sourceFile);
    const localName = element.name.getText(sourceFile);
    const exportEntry = exportMap.get(requestedName);
    const formattedRequested = formatSpecifier(requestedName, localName);
    const formattedSpecifier = exportEntry ? formatSpecifier(exportEntry.importName, localName) : formattedRequested;
    const specIsTypeOnly = clauseIsTypeOnly || Boolean((element as any).isTypeOnly);
    if (!exportEntry) {
      if (specIsTypeOnly) {
        remainingTypeSpecifiers.push(formattedRequested);
      } else {
        remainingValueSpecifiers.push(formattedRequested);
      }
      continue;
    }
    hasRewrite = true;
    const newSource = toRelativeModulePath(moduleSpecifier, exportEntry.reexportPath);
    const specGroup = groupedImports.get(newSource) ?? { value: [], type: [] };
    if (specIsTypeOnly) {
      specGroup.type.push(formattedSpecifier);
    } else {
      specGroup.value.push(formattedSpecifier);
    }
    groupedImports.set(newSource, specGroup);
  }

  if (!hasRewrite) {
    return null;
  }

  const replacementParts: string[] = [];
  if (remainingValueSpecifiers.length > 0) {
    replacementParts.push(buildImportStatement(moduleSpecifier, remainingValueSpecifiers));
  }
  if (remainingTypeSpecifiers.length > 0) {
    replacementParts.push(buildImportStatement(moduleSpecifier, remainingTypeSpecifiers, true));
  }
  for (const [source, specs] of groupedImports.entries()) {
    if (specs.value.length > 0) {
      replacementParts.push(buildImportStatement(source, specs.value));
    }
    if (specs.type.length > 0) {
      replacementParts.push(buildImportStatement(source, specs.type, true));
    }
  }

  const summary = Array.from(groupedImports.entries())
    .map(([source, specs]) => {
      const count = specs.value.length + specs.type.length;
      return `${source}(${count})`;
    })
    .join(', ');
  logInfo(`${toWorkspaceRelative(filePath)} expanded '${moduleSpecifier}' -> ${summary}`);

  const start = node.getFullStart();
  const end = node.getEnd();
  const leadingTrivia = sourceFile.text.slice(node.getFullStart(), node.getStart());
  const replacementText = `${leadingTrivia}${replacementParts.join('\n')}`;
  return { start, end, text: replacementText };
}

async function runTransform(thisContext: any, code: string, id: string): Promise<CompilePluginResult | null> {
  if (!id.endsWith('.ts') && !id.endsWith('.ets')) {
    return null;
  }
  const sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
  const importNodes: ts.ImportDeclaration[] = [];
  sourceFile.forEachChild((node) => {
    if (ts.isImportDeclaration(node)) {
      importNodes.push(node);
    }
  });

  const replacements: Array<{ start: number; end: number; text: string }> = [];
  for (const node of importNodes) {
    const rewritten = await rewriteImportDeclaration(thisContext, node, sourceFile, id);
    if (rewritten) {
      replacements.push(rewritten);
    }
  }

  if (replacements.length === 0) {
    return null;
  }

  replacements.sort((a, b) => a.start - b.start);
  let cursor = 0;
  let transformed = '';
  for (const { start, end, text } of replacements) {
    transformed += code.slice(cursor, start);
    transformed += text;
    cursor = end;
  }
  transformed += code.slice(cursor);

  return {
    code: transformed,
    map: null,
  };
}

const indexExpandCompilePlugin: CompilePlugin = {
  name: 'index-expand-plugin',
  async transform(this: any, code: string, id: string) {
    try {
      return await runTransform(this, code, id);
    } catch (error) {
      console.error('[IndexExpandCompilePlugin] transform error:', error);
      return null;
    }
  },
};

export default indexExpandCompilePlugin;
